﻿/**
*	Projector
*	@author 	Liam O'Donnell 2007-12-18
**/

package com.spikything.pants3d.core {

	import com.spikything.pants3d.*;
	import com.spikything.pants3d.core.*;
	import com.spikything.pants3d.events.*;
	import com.spikything.pants3d.objects.*;
	import flash.display.DisplayObject;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.filters.BlurFilter;
	import flash.geom.ColorTransform;
	import flash.ui.Keyboard;
	import flash.utils.getTimer;

	public class Projector {

		public var canvas 			:Canvas; 			// Where we add and render our Sprite3Ds
		public var sprites 			:Array 				// An array of Sprite3Ds on our canvas
		public var groups 			:Array 				// An array of groups of Sprite3Ds
		public var polygons 		:Array 				// An array of Polygon objects
		public var zSort 			:Number; 			// Z-sorting flag (average frames between z-sort updates, 0 for no z-sorting)
		public var mouseControlled	:Boolean;			// Mouse controls enabled
		public var ready			:Boolean;			// Controls/interactivity enabled
		public var autoUpdateSprites:Boolean 	= true;// Automatically updates sprite positions in render loop?
		public var elevation 		:Number 	= 50; 	// Height camera floats above target height, ty
		public var camTarget 		:IRenderable; 		// A Point3D or Sprite3D the camera homes in on
		public var camTargetOffset 	:Point3D; 			// A Point3D object of the xyz distances the camera maintains from camTarget
		public var keysEnabled		:Boolean;			// Manual key controls enabled
		
		private var cameraTilt 		:Number; 			// Amount of camera tilt on back/forth movement
		private var cameraRoll 		:Number; 			// Amount of camera roll on left/right movement
		private var cameraRotation 	:Number; 			// Amount of camera rotation on left/right movement
		private var viewDist		:Number;			// Camera focal-length/view-angle
		private var maxDist			:Number;			// Maximum distance a sprite is visible
		private var minDist			:Number;			// Minimum distance a sprite is visible
		private var fadeFactor		:Number;			// Minimum focal range divisor (closeup fading)
		private var minAlpha		:Number;			// Minimum closeup alpha before invisible
		private var depthQueuing	:Boolean;			// Do we fade sprites into background?
		private var noAlpha 		:Boolean;			// Override alpha fading?
		private var depthQueueR		:Number;			// Background fade RED component (0-1)
		private var depthQueueG		:Number;			// Background fade GREEN component (0-1)
		private var depthQueueB		:Number;			// Background fade BLUE component (0-1)
		private var depthQueueFactor:Number;			// Light intensity gradient
		private var maxLuminosity	:Number;			// Light intensity limit
		private var px				:Number;			// Camera X pos
		private var py				:Number;			// Camera Y pos
		private var pz				:Number;			// Camera Z pos
		private var vx				:Number;			// Camera X speed
		private var vy				:Number;			// Camera Y speed
		private var vz				:Number;			// Camera Z speed
		private var rx				:Number;			// Camera X rotation
		private var ry				:Number;			// Camera Y rotation
		private var rz				:Number;			// Camera Z rotation
		private var vrx				:Number;			// Camera X rotation speed
		private var vry				:Number;			// Camera Y rotation speed
		private var vrz				:Number;			// Camera Z rotation speed
		//private var tx				:Number;			// Camera target X
		//private var ty				:Number;			// Camera target Y
		//private var tz				:Number;			// Camera target Z
		private var rounding		:Number;			// Camera position granularity
		private var mouseSwayX 		:Number; 			// Camera motion sway
		private var mouseSwayY 		:Number;			// Camera motion sway
		private var damping			:Number;			// Camera seek damping
		private var friction		:Number;			// Camera air friction
		//private var gravity			:Number;			// Physics global gravity
		//private var elastic			:Number;			// Physics coefficient of elasticity
		private var keyX 			:Number; 			// Flag for LEFT/RIGHT keys
		private var keyY 			:Number; 			// Flag for UP/DOWN keys
		private var isEnabled		:Boolean;			// Simulation active/visible
		//private var isPaused 		:Boolean;			// Simulation paused
		private var gridSpacing		:Number		= 90;	// Test grid spacing
		private var gridSize		:Number		= 10;	// Test grid size
		private var gridWidth		:Number		= 700;	// Test grid width
		
		public function Projector (canvas:Canvas) {
			this.canvas = canvas;
			groups = new Array();
			sprites = new Array();
			polygons = new Array();
			disable();
			init();
		}
		
		// Sets up the camera, physics and render loop
		private function init ():void {
			setCamProps(200, 600, 5, .05, 50);//, 9, .6, .2, .2);
			setCamPos(0, 0, 0);
			setCamRotation(0, 0, 0);
			//setCamTarget(new Point3D(0, 0, 0));
			camTargetOffset = new Point3D(0, 0, 100);
			setCamMotion(0, 0, 0, 0, 0, 0);
			setDepthQueueColours(0, 0, 0, .1);
			//setPhysicsProps(9, .6, 3, .3);
			depthQueuing = true;
			noAlpha = false;
			cameraTilt = -4;
			cameraRoll = 1 / 400;
			cameraRotation = 1 / 100;
			rounding = 0;
			zSort = 1;
			keyX = keyY = 0;
			canvas.stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownEvent, false, 0, true);
			canvas.stage.addEventListener(KeyboardEvent.KEY_UP, keyUpEvent, false, 0, true);
			//canvas.addEventListener(Event.ENTER_FRAME, update, false, 0, true);
			//update();
		}
		
		public function kill ():void {
			for (var i:* in this) this[i] = null;
			sprites = new Array();
			groups = new Array();
			polygons = new Array();
		}
		
		// Draw a test calibration grid
		public function renderTestGrid ():void {
			var i:int;
			/*
			for (i=0; i<gridSize; i++) addSprite(null, gridWidth,	0,			gridSpacing*i-(gridSize/2), Dot);
			for (i=0; i<gridSize; i++) addSprite(null, -gridWidth,	0,			gridSpacing*i-(gridSize/2), Dot);
			for (i=0; i<gridSize; i++) addSprite(null, gridWidth,	-gridWidth,	gridSpacing*i-(gridSize/2), Dot);
			for (i=0; i<gridSize; i++) addSprite(null, -gridWidth,	-gridWidth,	gridSpacing*i-(gridSize/2), Dot);
			*/
			/*
			//for (i=0; i<gridSize; i++) addSprite(null, gridWidth,	gridWidth,	gridSpacing*i-(gridSize/2), Dot);
			//for (i=0; i<gridSize; i++) addSprite(null, -gridWidth,	gridWidth,	gridSpacing*i-(gridSize/2), Dot);
			//for (i=0; i<gridSize; i++) addSprite(null, 0,			gridWidth,	gridSpacing*i-(gridSize/2), Dot);
			//for (i=0; i<gridSize; i++) addSprite(null, 0,			-gridWidth,	gridSpacing*i-(gridSize/2), Dot);
			*/
			var j:int;
			for (i=0; i<gridSize; i++) addSprite(null, 0, 0, gridSpacing*i-250, Dot);
			for (j=1; j<15; j++) {
				for (i=0; i<gridSize; i++) addSprite(null, 50*j, 0, gridSpacing*i-250, Dot);
				for (i=0; i<gridSize; i++) addSprite(null, -50*j, 0, gridSpacing*i-250, Dot);
			}
		}
		
		// Add some test balls to the canvas
		public function addRandomSprites (what:Class, number:int=20, spread:Number=500):void {
			for (var i:int=0; i<number; i++) addSprite(null, Math.random()*spread-(spread/2), -Math.random()*spread-(spread/2), Math.random()*spread-(spread/2), what);
		}

		// Adjust camera settings (view distance, max distance, fade factor, min alpha)
		public function setCamProps (viewDist:Number, maxDist:Number, fadeFactor:Number, minAlpha:Number, minDist:Number, damping:Number=9, friction:Number=.6, mouseSwayX:Number=0, mouseSwayY:Number=0):void {
			this.viewDist = viewDist;
			this.maxDist = maxDist;
			this.fadeFactor = fadeFactor;
			this.minAlpha = minAlpha;
			this.minDist = minDist;
			this.damping = damping;
			this.friction = friction;
			this.mouseSwayX = mouseSwayX;
			this.mouseSwayY = mouseSwayY;
		}

		public function setCamMotion (mx:Number, my:Number, mz:Number, vrx:Number=0, vry:Number=0, vrz:Number=0):void {
			vx = mx;
			vy = my;
			vz = mz;
			this.vrx = vrx;
			this.vry = vry;
			this.vrz = vrz;
		}

		public function setCamTarget (what:IRenderable):void { //mx:Number, my:Number, mz:Number
			camTarget = what;
			//tx = mx;
			//ty = my;
			//tz = mz;
		}

		public function setCamPos (mx:Number, my:Number, mz:Number):void {
			px = mx;
			py = my;
			pz = mz;
		}
		
		public function setCamRotation (rx:Number, ry:Number, rz:Number):void {
			this.rx = rx;
			this.ry = ry;
			this.rz = rz;
		}
		
		public function setDepthQueueColours (r:Number, g:Number, b:Number, df:Number=1, ml:Number=1):void {
			depthQueueR = r;
			depthQueueG = g;
			depthQueueB = b;
			depthQueueFactor = df;
			maxLuminosity = ml;
		}

		/*
		public function setPhysicsProps (td:Number=9, tf:Number=.6, tg:Number=3, te:Number=.3):void {
			//damping = td;
			//friction = tf;
			//gravity = tg;
			Physics.gravity = tg;
			Physics.elasticity = te;
		}
		*/

		public function enable ():void {
			canvas.visible = isEnabled = true;
		}

		public function disable ():void {
			canvas.visible = isEnabled = false;
		}
		
		public function addSprite (child:Sprite = null, x:Number = 0, y:Number = 0, z:Number = 0, myClass:Class = null, wrap:Boolean = false):Sprite3D {
			if (!myClass) myClass = Sprite3D;
			var m:Sprite3D = new myClass(x, y, z, child);
			canvas.addChild(m);
			sprites.push(m);
			return m;
		}
		
		public function removeSprite (which:IRenderable):void {
			try { canvas.removeChild(Sprite(which)) } finally {};
			var i:int;
			for (i = 0; i < sprites.length; i++) {
				if (sprites[i] == which) {
					sprites.splice(i, 1);
					return;
				}
			}
		}
		
		public function startLoop ():void { canvas.addEventListener(Event.ENTER_FRAME, update, false, 0, true) };
		public function stopLoop ():void { canvas.removeEventListener(Event.ENTER_FRAME, update) };
		
		// The main render method
		public function update (e:Event=null):void {

			if (!isEnabled) return; // || isPaused
			
			// Key controls
			//elevation -= keyY * 8;
			//viewDist += keyX;
			px -= keyX * 8;
			py -= keyY * 8;
			//trace("px:"+px+", py:"+py+", pz:"+pz+", viewDist:"+viewDist+", rx:"+rx);

			// Mouse controls
			if (mouseControlled && ready) {
				camTarget.px -= canvas.mouseX/12;
				//ry -= canvas.mouseX/1000;
				camTarget.pz += canvas.mouseY/24;
				camTarget.py = elevation + vz * cameraTilt;
				//rx = -vz/40;
				rz = vx * cameraRoll;
				ry = vx * cameraRotation;
			}

			// Chasecam
			if (camTarget) {
				vx += (camTargetOffset.px-camTarget.px-px-(canvas.mouseX*mouseSwayX))/damping;
				vy += (camTargetOffset.py-camTarget.py-py-(canvas.mouseY*mouseSwayY))/damping;
				vz += (camTargetOffset.pz-camTarget.pz-pz)/damping;
			}
			
			// Camera movement
			vx *= friction;
			vy *= friction;
			vz *= friction;
			px += vx;
			py += vy;
			pz += vz;
			rx += vrx;
			ry += vry;
			rz += vrz;
			
			// Rounding
			if (rounding) {
				px = Math.round(px*rounding)/rounding;
				py = Math.round(py*rounding)/rounding;
				pz = Math.round(pz*rounding)/rounding;
			}

			// Camera rotation 3-D matrix transformation
			var trans:Transformation = new Transformation(rx, ry, rz);
			
			// Scene rotation test
			//var rotate:Transformation = new Transformation(.2, .1, .05);
			//rotate.transformPoints(sprites, new Point3D(0, 0, 0));
			
			// Loop through updating groups and rendering all sprites (then polygons) to the canvas
			var i:int;
			for (i=0; i < groups.length; i++) groups[i].update();
			for (i=0; i < sprites.length; i++) { // (var i:int = 0; i < canvas.numChildren; i++ )
				var m:Sprite3D = sprites[i]; // canvas.getChildAt(i) as Sprite3D;
				
				if (autoUpdateSprites) m.update();
				
				var mx:Number = m.px + px;
				var my:Number = m.py + py;
				var mz:Number = m.pz + pz;
				
				var point:Point3D = new Point3D(mx, my, mz);
				trans.transformPoint(point, new Point3D(0,0,0));
				
				mx = point.x;
				my = point.y;
				mz = point.z;

				// Popup check
				if (mz > maxDist || mz<=0 || !m.isVisible) {
					m.visible = false;
					m.filters = [];
					continue;
				} else {
					m.visible = true;
				}

				// Render
				var p:Number = viewDist / mz;
				m.scaleX = m.scaleY = p;
				m.x = mx * p;
				m.y = my * p;
				
				// Flat Plane3D adjustment
				if (m.spriteType == Pants3D.FLOOR) {
					var sPlane:Number = Math.sin(Math.atan(my / mz) + rx);
					m.scaleY = p * sPlane;
					//trace("my/mx:" + (my / mz) + " rx:" + rx + " sPlane:" + sPlane);
				}
				
				// Depth-queue tinting and lighting
				if (depthQueuing) {
					var d:Number = (mz/maxDist);
					var ca:Number = Math.min((100-(100*d))*depthQueueFactor, maxLuminosity);
					var co:Number = 1;// 255 * d * depthQueueFactor;
					var ct:ColorTransform = m.transform.colorTransform;
					ct.redMultiplier 	= ca + m.getSelfLighting("r");
					ct.greenMultiplier 	= ca + m.getSelfLighting("g");
					ct.blueMultiplier 	= ca + m.getSelfLighting("b");
					ct.redOffset 		= co * depthQueueR;
					ct.greenOffset 		= co * depthQueueG;
					ct.blueOffset 		= co * depthQueueB;
					m.transform.colorTransform = ct;
				}

				// Close-up fade/blur
				if (fadeFactor && mz<(100/fadeFactor)+minDist) {
					m.alpha = Math.min((mz*fadeFactor)-minDist, 100) / 100;
					
					var bl:Number = (1-m.alpha)*32;
					if (m.alpha<1 && Math.abs(mx)<100) {
						m.filters = [new BlurFilter(bl,bl,1)];
					} else {
						m.filters = [];
					}
					
					if (m.alpha<minAlpha) {
						m.visible = false;
						m.filters = [];
					}
				} else {
					m.filters = [];
					m.alpha = 1;
				}
				if (noAlpha) m.alpha = 1;

				// Sprite sounds
				// if (Math.abs(vz)>mz && Math.abs(vz)>5 && Math.abs(mx)<100 && m.numSounds) soundManager.playSound("snd_flyby_"+m.spriteType+random(m.numSounds),0,0,Math.min((Math.abs(vz)*10)-Math.abs(mx),100),mx);

			}
			
			// Render polygons
			for (i = 0; i < polygons.length; i++) (polygons[i] as Polygon).update();
			
			// Z-sorting
			var t:int = getTimer();
			if (zSort && Math.random()*zSort > zSort-1) {
				sprites.sortOn("scaleX", Array.NUMERIC);
				var j:int = sprites.length;
				while (j--) canvas.addChildAt(sprites[j], 0);
			}
			// trace("Z-sorting took "+(getTimer()-t)+" ms");
		}
		
		protected function keyDownEvent (event:KeyboardEvent):void {
			if (!keysEnabled) return;
			switch (event.keyCode) {
				case Keyboard.UP:
					keyY = 1;
				break;
	
				case Keyboard.DOWN:
					keyY = -1;
				break;
	
				case Keyboard.LEFT:
					keyX = -1;
				break;
	
				case Keyboard.RIGHT:
					keyX = 1;
				break;
				
				case Keyboard.PAGE_UP:
					rx += .01;
				break;
				
				case Keyboard.PAGE_DOWN:
					rx -= .01;
				break;
				
				case Keyboard.HOME:
					pz -= 2;
				break;
				
				case Keyboard.END:
					pz += 2;
				break;
			}
		}
		
		protected function keyUpEvent (event:KeyboardEvent):void {
			switch (event.keyCode) {
				case Keyboard.UP:
				case Keyboard.DOWN:
					keyY = 0;
				break;
		
				case Keyboard.LEFT:
				case Keyboard.RIGHT:
					keyX = 0;
				break;
			}
		}

	}
	
}